{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}

module Bots.DirectoryTests where

import ChatClient
import ChatTests.DBUtils
import ChatTests.Utils
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Exception (finally)
import Control.Monad (forM_, when)
import qualified Data.Text as T
import Directory.Captcha
import Directory.Options
import Directory.Service
import Directory.Store
import GHC.IO.Handle (hClose)
import Simplex.Chat.Bot.KnownContacts
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks)
import Simplex.Chat.Core
import qualified Simplex.Chat.Markdown as MD
import Simplex.Chat.Options (CoreChatOpts (..))
import Simplex.Chat.Options.DB
import Simplex.Chat.Types (ChatPeerType (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import System.FilePath ((</>))
import Test.Hspec hiding (it)

directoryServiceTests :: SpecWith TestParams
directoryServiceTests = do
  it "should register group" testDirectoryService
  it "should suspend and resume group, send message to owner" testSuspendResume
  it "should delete group registration" testDeleteGroup
  it "admin should delete group registration" testDeleteGroupAdmin
  it "should change initial member role" testSetRole
  it "should join found group via link" testJoinGroup
  it "should support group names with spaces" testGroupNameWithSpaces
  it "should return more groups in search, all and recent groups" testSearchGroups
  it "should invite to owners' group if specified" testInviteToOwnersGroup
  describe "de-listing the group" $ do
    it "should de-list if owner leaves the group" testDelistedOwnerLeaves
    it "should de-list if owner is removed from the group" testDelistedOwnerRemoved
    it "should NOT de-list if another member leaves the group" testNotDelistedMemberLeaves
    it "should NOT de-list if another member is removed from the group" testNotDelistedMemberRemoved
    it "should de-list if service is removed from the group" testDelistedServiceRemoved
    it "should de-list if group is deleted" testDelistedGroupDeleted
    it "should de-list/re-list when service/owner roles change" testDelistedRoleChanges
    it "should NOT de-list if another member role changes" testNotDelistedMemberRoleChanged
    it "should NOT send to approval if roles are incorrect" testNotSentApprovalBadRoles
    it "should NOT allow approving if roles are incorrect" testNotApprovedBadRoles
  describe "should require re-approval if profile is changed by" $ do
    it "the registration owner" testRegOwnerChangedProfile
    it "another owner" testAnotherOwnerChangedProfile
    it "another owner not connected to directory" testNotConnectedOwnerChangedProfile
  describe "should require profile update if group link is removed by " $ do
    it "the registration owner" testRegOwnerRemovedLink
    it "another owner" testAnotherOwnerRemovedLink
    it "another owner not connected to directory" testNotConnectedOwnerRemovedLink
  describe "duplicate groups (same display name and full name)" $ do
    it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation
    it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration
    it "should prohibit confirmation if a duplicate group is listed" testDuplicateProhibitConfirmation
    it "should prohibit when profile is updated and not send for approval" testDuplicateProhibitWhenUpdated
    it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval
  describe "list groups" $ do
    it "should list user's groups" testListUserGroups
  describe "member admission" $ do
    it "should ask member to pass captcha screen" testCapthaScreening
  describe "store log" $ do
    it "should restore directory service state" testRestoreDirectory
  describe "captcha" $ do
    it "should accept some incorrect spellings" testCaptcha

directoryProfile :: Profile
directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing}

mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> DirectoryOpts
mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup =
  DirectoryOpts
    { coreOptions =
        testCoreOpts
          { dbOptions =
              (dbOptions testCoreOpts)
#if defined(dbPostgres)
                {dbSchemaPrefix = "client_" <> serviceDbPrefix}
#else
                {dbFilePrefix = ps </> serviceDbPrefix}
#endif

          },
      adminUsers = [],
      superUsers,
      ownersGroup,
      blockedFragmentsFile = Nothing,
      blockedWordsFile = Nothing,
      blockedExtensionRules = Nothing,
      nameSpellingFile = Nothing,
      profileNameLimit = maxBound,
      captchaGenerator = Nothing,
      directoryLog = Just $ ps </> "directory_service.log",
      serviceName = "SimpleX Directory",
      runCLI = False,
      searchResults = 3,
      testing = True
    }

serviceDbPrefix :: FilePath
serviceDbPrefix = "directory_service"

viewName :: String -> String
viewName = T.unpack . MD.viewName . T.pack

testDirectoryService :: HasCallStack => TestParams -> IO ()
testDirectoryService ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        bob #> "@'SimpleX Directory' privacy"
        bob <# "'SimpleX Directory'> > privacy"
        bob <## "      No groups found"
        -- putStrLn "*** create a group"
        bob ##> "/g PSA Privacy, Security & Anonymity"
        bob <## "group #PSA (Privacy, Security & Anonymity) is created"
        bob <## "to add members use /a PSA <name> or /create link #PSA"
        bob ##> "/a PSA 'SimpleX Directory' member"
        bob <## "invitation to join the group #PSA sent to 'SimpleX Directory'"
        bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group"
        bob ##> "/mr PSA 'SimpleX Directory' admin"
        -- putStrLn "*** discover service joins group and creates the link for profile"
        bob <## "#PSA: you changed the role of 'SimpleX Directory' to admin"
        bob <# "'SimpleX Directory'> Joining the group PSA…"
        bob <## "#PSA: 'SimpleX Directory' joined the group"
        bob <# "'SimpleX Directory'> Joined the group PSA, creating the link…"
        bob <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
        bob <## ""
        bob <## "Please add it to the group welcome message."
        bob <## "For example, add:"
        welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob
        -- putStrLn "*** update profile without link"
        updateGroupProfile bob "Welcome!"
        bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message."
        (superUser </)
        -- putStrLn "*** update profile so that it has link"
        updateGroupProfile bob welcomeWithLink
        bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
        bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
        approvalRequested superUser welcomeWithLink (1 :: Int)
        -- putStrLn "*** update profile so that it still has link"
        let welcomeWithLink' = "Welcome! " <> welcomeWithLink
        updateGroupProfile bob welcomeWithLink'
        bob <# "'SimpleX Directory'> The group ID 1 (PSA) is updated!"
        bob <## "It is hidden from the directory until approved."
        superUser <# "'SimpleX Directory'> The group ID 1 (PSA) is updated."
        approvalRequested superUser welcomeWithLink' (2 :: Int)
        -- putStrLn "*** try approving with the old registration code"
        bob #> "@'SimpleX Directory' /approve 1:PSA 1"
        bob <# "'SimpleX Directory'> > /approve 1:PSA 1"
        bob <## "      You are not allowed to use this command"
        superUser #> "@'SimpleX Directory' /approve 1:PSA 1"
        superUser <# "'SimpleX Directory'> > /approve 1:PSA 1"
        superUser <## "      Incorrect approval code"
        -- putStrLn "*** update profile so that it has no link"
        updateGroupProfile bob "Welcome!"
        bob <# "'SimpleX Directory'> The group link for ID 1 (PSA) is removed from the welcome message."
        bob <## ""
        bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
        superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (PSA), de-listed."
        superUser #> "@'SimpleX Directory' /approve 1:PSA 2"
        superUser <# "'SimpleX Directory'> > /approve 1:PSA 2"
        superUser <## "      Error: the group ID 1 (PSA) is not pending approval."
        -- putStrLn "*** update profile so that it has link again"
        updateGroupProfile bob welcomeWithLink'
        bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
        bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
        approvalRequested superUser welcomeWithLink' (1 :: Int)
        superUser #> "@'SimpleX Directory' /pending"
        superUser <# "'SimpleX Directory'> > /pending"
        superUser <## "      1 registered group(s)"
        superUser <# "'SimpleX Directory'> 1. PSA (Privacy, Security & Anonymity)"
        superUser <## "Welcome message:"
        superUser <##. "Welcome! Link to join the group PSA: "
        superUser <## "Owner: bob"
        superUser <## "2 members"
        superUser <## "Status: pending admin approval"
        superUser <## "/'role 1', /'filter 1'"
        superUser #> "@'SimpleX Directory' /approve 1:PSA 1"
        superUser <# "'SimpleX Directory'> > /approve 1:PSA 1"
        superUser <## "      Group approved!"
        bob <# "'SimpleX Directory'> The group ID 1 (PSA) is approved and listed in directory - please moderate it!"
        bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
        bob <## ""
        bob <## "Supported commands:"
        bob <## "/'filter 1' - to configure anti-spam filter."
        bob <## "/'role 1' - to set default member role."
        bob <## "/'link 1' - to view/upgrade group link."
        search bob "privacy" welcomeWithLink'
        search bob "security" welcomeWithLink'
        cath `connectVia` dsLink
        search cath "privacy" welcomeWithLink'
        bob #> "@'SimpleX Directory' /exec /contacts"
        bob <# "'SimpleX Directory'> > /exec /contacts"
        bob <## "      You are not allowed to use this command"
        superUser #> "@'SimpleX Directory' /exec /contacts"
        superUser <# "'SimpleX Directory'> > /exec /contacts"
        superUser <## "      alice (Alice)"
        superUser <## "bob (Bob)"
        superUser <## "cath (Catherine)"
  where
    search u s welcome = do
      u #> ("@'SimpleX Directory' " <> s)
      u <# ("'SimpleX Directory'> > " <> s)
      u <## "      Found 1 group(s)."
      u <# "'SimpleX Directory'> PSA (Privacy, Security & Anonymity)"
      u <## "Welcome message:"
      u <## welcome
      u <## "2 members"
    updateGroupProfile u welcome = do
      u ##> ("/set welcome #PSA " <> welcome)
      u <## "welcome message changed to:"
      u <## welcome
    approvalRequested su welcome grId = do
      su <# "'SimpleX Directory'> bob submitted the group ID 1:"
      su <## "PSA (Privacy, Security & Anonymity)"
      su <## "Welcome message:"
      su <## welcome
      su <## "2 members"
      su <## ""
      su <## "To approve send:"
      su <# ("'SimpleX Directory'> /approve 1:PSA " <> show grId)

testSuspendResume :: HasCallStack => TestParams -> IO ()
testSuspendResume ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob -> do
      bob `connectVia` dsLink
      registerGroup superUser bob "privacy" "Privacy"
      groupFound bob "privacy"
      superUser #> "@'SimpleX Directory' /suspend 1:privacy"
      superUser <# "'SimpleX Directory'> > /suspend 1:privacy"
      superUser <## "      Group suspended!"
      bob <# "'SimpleX Directory'> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators."
      groupNotFound bob "privacy"
      superUser #> "@'SimpleX Directory' /resume 1:privacy"
      superUser <# "'SimpleX Directory'> > /resume 1:privacy"
      superUser <## "      Group listing resumed!"
      bob <# "'SimpleX Directory'> The group ID 1 (privacy) is listed in the directory again!"
      groupFound bob "privacy"
      superUser #> "@'SimpleX Directory' privacy"
      groupFoundN_ "" (Just 1) 2 superUser "privacy"
      superUser #> "@'SimpleX Directory' /link 1:privacy"
      superUser <# "'SimpleX Directory'> > /link 1:privacy"
      superUser <## "      The link to join the group ID 1 (privacy):"
      superUser <##. "https://localhost/g#"
      superUser <## "New member role: member"
      -- get and change the link to the equivalent - should not ask to re-approve
      bob #> "@'SimpleX Directory' /link 1"
      bob <# "'SimpleX Directory'> > /link 1"
      bob <## "      The link to join the group ID 1 (privacy):"
      gLink <- getTermLine bob
      gLink `shouldStartWith` "https://localhost/g#"
      bob <## "New member role: member"
      bob ##> "/show welcome #privacy"
      bob <## "Welcome message:"
      bob <## ("Link to join the group privacy: " <> gLink)
      bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink <> "?same_link=true")
      bob <## "welcome message changed to:"
      bob <## ("Link to join the group privacy: " <> gLink <> "?same_link=true")
      bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
      bob <## "The group is listed in directory."
      superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
      superUser <## "The group remained listed in directory."
      -- upgrade link
      -- make it upgradeable first
      superUser #> "@'SimpleX Directory' /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
      superUser <# "'SimpleX Directory'> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
      superUser <## ""
      bob #> "@'SimpleX Directory' /link 1"
      bob <# "'SimpleX Directory'> > /link 1"
      bob <## "      The link to join the group ID 1 (privacy):"
      bob <##. "https://simplex.chat/contact#/"
      bob <## "New member role: member"
      bob <## "The link is being upgraded..."
      bob <# "'SimpleX Directory'> Please replace the old link in welcome message of your group ID 1 (privacy)"
      bob <## "If this is the only change, the group will remain listed in directory without re-approval."
      bob <## ""
      bob <## "The new link:"
      gLink' <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob
      bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink')
      bob <## "welcome message changed to:"
      bob <## ("Link to join the group privacy: " <> gLink')
      bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
      bob <## "The group is listed in directory."
      superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
      superUser <## "The group remained listed in directory."
      -- send message to group owner
      superUser #> "@'SimpleX Directory' /owner 1:privacy hello there"
      superUser <# "'SimpleX Directory'> > /owner 1:privacy hello there"
      superUser <## "      Forwarded to @bob, the owner of the group ID 1 (privacy)"
      bob <# "'SimpleX Directory'> hello there"

testDeleteGroup :: HasCallStack => TestParams -> IO ()
testDeleteGroup ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob -> do
      bob `connectVia` dsLink
      registerGroup superUser bob "privacy" "Privacy"
      groupFound bob "privacy"
      bob #> "@'SimpleX Directory' /delete 1:privacy"
      bob <# "'SimpleX Directory'> > /delete 1:privacy"
      bob <## "      Your group privacy is deleted from the directory"
      groupNotFound bob "privacy"

testDeleteGroupAdmin :: HasCallStack => TestParams -> IO ()
testDeleteGroupAdmin ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob -> do
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        cath `connectVia` dsLink
        registerGroupId superUser cath "security" "Security" 2 1
        groupFound bob "privacy"
        groupFound bob "security"
        listUserGroup bob "privacy" "Privacy"
        listUserGroup cath "security" "Security"
        superUser #> "@'SimpleX Directory' /last"
        superUser <# "'SimpleX Directory'> > /last"
        superUser <## "      2 registered group(s)"
        memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active"
        memberGroupListing superUser cath 2 "security" "Security" 2 "active"
        -- trying to register group with the same name
        submitGroup bob "security" "Security"
        bob <# "'SimpleX Directory'> The group security (Security) is already listed in the directory, please choose another name."
        bob ##> "/d #security"
        bob <## "#security: you deleted the group"
        -- admin can delete the group
        superUser #> "@'SimpleX Directory' /delete 2:security"
        superUser <# "'SimpleX Directory'> > /delete 2:security"
        superUser <## "      The group security is deleted from the directory"
        groupFound cath "privacy"
        listUserGroup bob "privacy" "Privacy"
        groupNotFound bob "security"
        sendListCommand cath 0
        -- another user can register the group with the same name
        registerGroupId superUser bob "security" "Security" 4 2

testSetRole :: HasCallStack => TestParams -> IO ()
testSetRole ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        groupFound bob "privacy"
        bob #> "@'SimpleX Directory' /role 1:privacy observer"
        bob <# "'SimpleX Directory'> > /role 1:privacy observer"
        bob <## "      The initial member role for the group privacy is set to observer"
        bob <## ""
        note <- getTermLine bob
        let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
        cath ##> ("/c " <> groupLink)
        cath <## "connection request sent!"
        cath <## "#privacy: joining the group..."
        cath <## "#privacy: you joined the group"
        cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#"
        cath <## "#privacy: member bob (Bob) is connected"
        bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
        bob <## "#privacy: new member cath is connected"
        bob ##> "/ms #privacy"
        bob <## "bob (Bob): owner, you, created group"
        bob <## "'SimpleX Directory': admin, invited, connected"
        bob <## "cath (Catherine): observer, connected"
        cath ##> "#privacy hello"
        cath <## "#privacy: you don't have permission to send messages"

testJoinGroup :: HasCallStack => TestParams -> IO ()
testJoinGroup ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob -> do
      withNewTestChat ps "cath" cathProfile $ \cath ->
        withNewTestChat ps "dan" danProfile $ \dan -> do
          bob `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          cath `connectVia` dsLink
          cath #> "@'SimpleX Directory' privacy"
          cath <# "'SimpleX Directory'> > privacy"
          cath <## "      Found 1 group(s)."
          cath <# "'SimpleX Directory'> privacy (Privacy)"
          cath <## "Welcome message:"
          welcomeMsg <- getTermLine cath
          let groupLink = dropStrPrefix "Link to join the group privacy: " welcomeMsg
          cath <## "2 members"
          cath ##> ("/c " <> groupLink)
          cath <## "connection request sent!"
          cath <## "#privacy: joining the group..."
          cath <## "#privacy: you joined the group"
          cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
          cath <## "use @'SimpleX Directory' <message> to send messages"
          cath <# ("#privacy 'SimpleX Directory'> " <> welcomeMsg)
          cath <## "#privacy: member bob (Bob) is connected"
          bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
          bob <## "#privacy: new member cath is connected"
          bob ##> "/create link #privacy"
          bobLink <- getGroupLink bob "privacy" GRMember True
          dan ##> ("/c " <> bobLink)
          dan <## "connection request sent!"
          concurrentlyN_
            [ do
                bob <## "dan (Daniel): accepting request to join group #privacy..."
                bob <## "#privacy: dan joined the group",
              do
                dan <## "#privacy: joining the group..."
                dan <## "#privacy: you joined the group"
                dan <# ("#privacy bob> " <> welcomeMsg)
                dan
                  <### [ "#privacy: member 'SimpleX Directory' is connected",
                         "#privacy: member cath (Catherine) is connected"
                       ],
              do
                cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)"
                cath <## "#privacy: new member dan is connected"
            ]

testGroupNameWithSpaces :: HasCallStack => TestParams -> IO ()
testGroupNameWithSpaces ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob -> do
      bob `connectVia` dsLink
      registerGroup superUser bob "Privacy & Security" ""
      groupFound bob "Privacy & Security"
      superUser #> "@'SimpleX Directory' /suspend 1:'Privacy & Security'"
      superUser <# "'SimpleX Directory'> > /suspend 1:'Privacy & Security'"
      superUser <## "      Group suspended!"
      bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is suspended and hidden from directory. Please contact the administrators."
      groupNotFound bob "privacy"
      superUser #> "@'SimpleX Directory' /resume 1:'Privacy & Security'"
      superUser <# "'SimpleX Directory'> > /resume 1:'Privacy & Security'"
      superUser <## "      Group listing resumed!"
      bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is listed in the directory again!"
      groupFound bob "Privacy & Security"

testSearchGroups :: HasCallStack => TestParams -> IO ()
testSearchGroups ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob -> do
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        cath `connectVia` dsLink
        forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i
        connectUsers bob cath
        fullAddMember "MyGroup" "" bob cath GRMember
        joinGroup "MyGroup" cath bob
        cath <## "#MyGroup: member 'SimpleX Directory_1' is connected"
        cath <## "contact and member are merged: 'SimpleX Directory', #MyGroup 'SimpleX Directory_1'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        cath #> "@'SimpleX Directory' MyGroup"
        cath <# "'SimpleX Directory'> > MyGroup"
        cath <## "      Found 7 group(s), sending top 3."
        receivedGroup cath 0 3
        receivedGroup cath 1 2
        receivedGroup cath 2 2
        cath <# "'SimpleX Directory'> Send /next for 4 more result(s)."
        cath #> "@'SimpleX Directory' /next"
        cath <# "'SimpleX Directory'> > /next"
        cath <## "      Sending 3 more group(s)."
        receivedGroup cath 3 2
        receivedGroup cath 4 2
        receivedGroup cath 5 2
        cath <# "'SimpleX Directory'> Send /next for 1 more result(s)."
        -- search of another user does not affect the search of the first user
        groupFound bob "Another"
        cath #> "@'SimpleX Directory' ."
        cath <# "'SimpleX Directory'> > ."
        cath <## "      Sending 1 more group(s)."
        receivedGroup cath 6 2
        cath #> "@'SimpleX Directory' /all"
        cath <# "'SimpleX Directory'> > /all"
        cath <## "      8 group(s) listed, sending top 3."
        receivedGroup cath 0 3
        receivedGroup cath 1 2
        receivedGroup cath 2 2
        cath <# "'SimpleX Directory'> Send /next for 5 more result(s)."
        cath #> "@'SimpleX Directory' /new"
        cath <# "'SimpleX Directory'> > /new"
        cath <## "      8 group(s) listed, sending the most recent 3."
        receivedGroup cath 7 2
        receivedGroup cath 6 2
        receivedGroup cath 5 2
        cath <# "'SimpleX Directory'> Send /next for 5 more result(s)."
        cath #> "@'SimpleX Directory' term3"
        cath <# "'SimpleX Directory'> > term3"
        cath <## "      Found 3 group(s)."
        receivedGroup cath 4 2
        receivedGroup cath 5 2
        receivedGroup cath 6 2
        cath #> "@'SimpleX Directory' term1"
        cath <# "'SimpleX Directory'> > term1"
        cath <## "      Found 6 group(s), sending top 3."
        receivedGroup cath 1 2
        receivedGroup cath 2 2
        receivedGroup cath 3 2
        cath <# "'SimpleX Directory'> Send /next for 3 more result(s)."
        cath #> "@'SimpleX Directory' ."
        cath <# "'SimpleX Directory'> > ."
        cath <## "      Sending 3 more group(s)."
        receivedGroup cath 4 2
        receivedGroup cath 5 2
        receivedGroup cath 6 2
  where
    groups :: [String]
    groups =
      [ "MyGroup",
        "MyGroup term1 1",
        "MyGroup term1 2",
        "MyGroup term1 term2",
        "MyGroup term1 term2 term3",
        "MyGroup term1 term2 term3 term4",
        "MyGroup term1 term2 term3 term4 term5",
        "Another"
      ]
    receivedGroup :: TestCC -> Int -> Int -> IO ()
    receivedGroup u ix count = do
      u <#. ("'SimpleX Directory'> " <> groups !! ix)
      u <## "Welcome message:"
      u <##. "Link to join the group "
      u <## (show count <> " members")

testInviteToOwnersGroup :: HasCallStack => TestParams -> IO ()
testInviteToOwnersGroup ps =
  withDirectoryServiceCfgOwnersGroup ps testCfg True $ \superUser dsLink ->
    withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do
      bob `connectVia` dsLink
      registerGroupId superUser bob "privacy" "Privacy" 2 1
      bob <## "#owners: 'SimpleX Directory' invites you to join the group as member"
      bob <## "use /j owners to accept"
      superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners"
      bob ##> "/j owners"
      bob <## "#owners: you joined the group"
      bob <## "#owners: member alice (Alice) is connected"
      superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)"
      superUser <## "#owners: new member bob is connected"
      -- second group
      registerGroupId superUser bob "security" "Security" 3 2
      superUser <## "Owner is already a member of owners' group"

testDelistedOwnerLeaves :: HasCallStack => TestParams -> IO ()
testDelistedOwnerLeaves ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        leaveGroup "privacy" bob
        cath <## "#privacy: bob left the group"
        bob <# "'SimpleX Directory'> You left the group ID 1 (privacy)."
        bob <## ""
        bob <## "The group is no longer listed in the directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner left)."
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        groupNotFound cath "privacy"

testDelistedOwnerRemoved :: HasCallStack => TestParams -> IO ()
testDelistedOwnerRemoved ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        removeMember "privacy" cath bob
        bob <# "'SimpleX Directory'> You are removed from the group ID 1 (privacy)."
        bob <## ""
        bob <## "The group is no longer listed in the directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner is removed)."
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        groupNotFound cath "privacy"

testNotDelistedMemberLeaves :: HasCallStack => TestParams -> IO ()
testNotDelistedMemberLeaves ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        leaveGroup "privacy" cath
        bob <## "#privacy: cath left the group"
        (superUser </)
        cath `connectVia` dsLink
        cath #> "@'SimpleX Directory_1' privacy"
        groupFoundN_ "_1" Nothing 2 cath "privacy"

testNotDelistedMemberRemoved :: HasCallStack => TestParams -> IO ()
testNotDelistedMemberRemoved ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        removeMember "privacy" bob cath
        (superUser </)
        cath `connectVia` dsLink
        cath #> "@'SimpleX Directory_1' privacy"
        groupFoundN_ "_1" Nothing 2 cath "privacy"

testDelistedServiceRemoved :: HasCallStack => TestParams -> IO ()
testDelistedServiceRemoved ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        bob ##> "/rm #privacy 'SimpleX Directory'"
        bob <## "#privacy: you removed 'SimpleX Directory' from the group"
        cath <## "#privacy: bob removed 'SimpleX Directory' from the group"
        bob <# "'SimpleX Directory'> SimpleX Directory is removed from the group ID 1 (privacy)."
        bob <## ""
        bob <## "The group is no longer listed in the directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (directory service is removed)."
        cath `connectVia` dsLink
        groupNotFound_ "_1" cath "privacy"

testDelistedGroupDeleted :: HasCallStack => TestParams -> IO ()
testDelistedGroupDeleted ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        cath `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        connectUsers bob cath
        fullAddMember "privacy" "Privacy" bob cath GROwner
        joinGroup "privacy" cath bob
        cath <## "#privacy: member 'SimpleX Directory_1' is connected"
        cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        bob ##> "/d #privacy"
        bob <## "#privacy: you deleted the group"
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is deleted."
        bob <## ""
        bob <## "The group is no longer listed in the directory."
        cath <## "#privacy: bob deleted the group"
        cath <## "use /d #privacy to delete the local copy of the group"
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group is deleted)."
        groupNotFound cath "privacy"

testDelistedRoleChanges :: HasCallStack => TestParams -> IO ()
testDelistedRoleChanges ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        groupFoundN 3 cath "privacy"
        -- de-listed if service role changed
        bob ##> "/mr privacy 'SimpleX Directory' member"
        bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
        cath <## "#privacy: bob changed the role of 'SimpleX Directory' from admin to member"
        bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to member."
        bob <## ""
        bob <## "The group is no longer listed in the directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (SimpleX Directory role is changed to member)."
        groupNotFound cath "privacy"
        -- re-listed if service role changed back without profile changes
        cath ##> "/mr privacy 'SimpleX Directory' admin"
        cath <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
        bob <## "#privacy: cath changed the role of 'SimpleX Directory' from member to admin"
        bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
        bob <## ""
        bob <## "The group is listed in the directory again."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (SimpleX Directory role is changed to admin)."
        groupFoundN 3 cath "privacy"
        -- de-listed if owner role changed
        cath ##> "/mr privacy bob admin"
        cath <## "#privacy: you changed the role of bob to admin"
        bob <## "#privacy: cath changed your role from owner to admin"
        bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to admin."
        bob <## ""
        bob <## "The group is no longer listed in the directory."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (user role is set to admin)."
        groupNotFound cath "privacy"
        -- re-listed if owner role changed back without profile changes
        cath ##> "/mr privacy bob owner"
        cath <## "#privacy: you changed the role of bob to owner"
        bob <## "#privacy: cath changed your role from admin to owner"
        bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to owner."
        bob <## ""
        bob <## "The group is listed in the directory again."
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (user role is set to owner)."
        groupFoundN 3 cath "privacy"

testNotDelistedMemberRoleChanged :: HasCallStack => TestParams -> IO ()
testNotDelistedMemberRoleChanged ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        groupFoundN 3 cath "privacy"
        bob ##> "/mr privacy cath member"
        bob <## "#privacy: you changed the role of cath to member"
        cath <## "#privacy: bob changed your role from owner to member"
        groupFoundN 3 cath "privacy"

testNotSentApprovalBadRoles :: HasCallStack => TestParams -> IO ()
testNotSentApprovalBadRoles ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        cath `connectVia` dsLink
        submitGroup bob "privacy" "Privacy"
        welcomeWithLink <- groupAccepted bob "privacy"
        bob ##> "/mr privacy 'SimpleX Directory' member"
        bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
        updateProfileWithLink bob "privacy" welcomeWithLink 1
        bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group"
        bob ##> "/mr privacy 'SimpleX Directory' admin"
        bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
        bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
        bob <## ""
        bob <## "The group is submitted for approval."
        notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
        groupNotFound cath "privacy"
        approveRegistration superUser bob "privacy" 1
        groupFound cath "privacy"

testNotApprovedBadRoles :: HasCallStack => TestParams -> IO ()
testNotApprovedBadRoles ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        cath `connectVia` dsLink
        submitGroup bob "privacy" "Privacy"
        welcomeWithLink <- groupAccepted bob "privacy"
        updateProfileWithLink bob "privacy" welcomeWithLink 1
        notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
        bob ##> "/mr privacy 'SimpleX Directory' member"
        bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
        let approve = "/approve 1:privacy 1"
        superUser #> ("@'SimpleX Directory' " <> approve)
        superUser <# ("'SimpleX Directory'> > " <> approve)
        superUser <## "      Group is not approved: SimpleX Directory is not an admin."
        groupNotFound cath "privacy"
        bob ##> "/mr privacy 'SimpleX Directory' admin"
        bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
        bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
        bob <## ""
        bob <## "The group is submitted for approval."
        notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
        approveRegistration superUser bob "privacy" 1
        groupFound cath "privacy"

testRegOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
testRegOwnerChangedProfile ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        bob ##> "/gp privacy privacy Privacy and Security"
        bob <## "description changed to: Privacy and Security"
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
        bob <## "It is hidden from the directory until approved."
        cath <## "bob updated group #privacy:"
        cath <## "description changed to: Privacy and Security"
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        groupNotFound cath "privacy"
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated."
        reapproveGroup 3 superUser bob
        groupFoundN 3 cath "privacy"

testAnotherOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
testAnotherOwnerChangedProfile ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        cath ##> "/gp privacy privacy Privacy and Security"
        cath <## "description changed to: Privacy and Security"
        bob <## "cath updated group #privacy:"
        bob <## "description changed to: Privacy and Security"
        bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!"
        bob <## "It is hidden from the directory until approved."
        groupNotFound cath "privacy"
        superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath."
        reapproveGroup 3 superUser bob
        groupFoundN 3 cath "privacy"

testNotConnectedOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
testNotConnectedOwnerChangedProfile ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        withNewTestChat ps "dan" danProfile $ \dan -> do
          bob `connectVia` dsLink
          dan `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          cath ##> "/gp privacy privacy Privacy and Security"
          cath <## "description changed to: Privacy and Security"
          bob <## "cath updated group #privacy:"
          bob <## "description changed to: Privacy and Security"
          bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!"
          bob <## "It is hidden from the directory until approved."
          groupNotFound dan "privacy"
          superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath."
          reapproveGroup 3 superUser bob
          groupFoundN 3 dan "privacy"

testRegOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
testRegOwnerRemovedLink ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        bob ##> "/show welcome #privacy"
        bob <## "Welcome message:"
        welcomeWithLink <- getTermLine bob
        bob ##> "/set welcome #privacy Welcome!"
        bob <## "welcome message changed to:"
        bob <## "Welcome!"
        bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message."
        bob <## ""
        bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
        cath <## "bob updated group #privacy:"
        cath <## "welcome message changed to:"
        cath <## "Welcome!"
        superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        groupNotFound cath "privacy"
        let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink
        bob ##> ("/set welcome #privacy " <> withChangedLink)
        bob <## "welcome message changed to:"
        bob <## withChangedLink
        bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
        bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
        cath <## "bob updated group #privacy:"
        cath <## "welcome message changed to:"
        cath <## withChangedLink
        reapproveGroup 3 superUser bob
        groupFoundN 3 cath "privacy"

testAnotherOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
testAnotherOwnerRemovedLink ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        addCathAsOwner bob cath
        cath `connectVia` dsLink
        cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        bob ##> "/show welcome #privacy"
        bob <## "Welcome message:"
        welcomeWithLink <- getTermLine bob
        cath ##> "/set welcome #privacy Welcome!"
        cath <## "welcome message changed to:"
        cath <## "Welcome!"
        bob <## "cath updated group #privacy:"
        bob <## "welcome message changed to:"
        bob <## "Welcome!"
        bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath."
        bob <## ""
        bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
        superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
        groupNotFound cath "privacy"
        cath ##> ("/set welcome #privacy " <> welcomeWithLink)
        cath <## "welcome message changed to:"
        cath <## welcomeWithLink
        bob <## "cath updated group #privacy:"
        bob <## "welcome message changed to:"
        bob <## welcomeWithLink
        bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath."
        bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
        reapproveGroup 3 superUser bob
        groupFoundN 3 cath "privacy"

testNotConnectedOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
testNotConnectedOwnerRemovedLink ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        withNewTestChat ps "dan" danProfile $ \dan -> do
          bob `connectVia` dsLink
          dan `connectVia` dsLink
          registerGroup superUser bob "privacy" "Privacy"
          addCathAsOwner bob cath
          bob ##> "/show welcome #privacy"
          bob <## "Welcome message:"
          welcomeWithLink <- getTermLine bob
          cath ##> "/set welcome #privacy Welcome!"
          cath <## "welcome message changed to:"
          cath <## "Welcome!"
          bob <## "cath updated group #privacy:"
          bob <## "welcome message changed to:"
          bob <## "Welcome!"
          bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath."
          bob <## ""
          bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
          superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
          groupNotFound dan "privacy"
          cath ##> ("/set welcome #privacy " <> welcomeWithLink)
          cath <## "welcome message changed to:"
          cath <## welcomeWithLink
          bob <## "cath updated group #privacy:"
          bob <## "welcome message changed to:"
          bob <## welcomeWithLink
          -- bob <# "'SimpleX Directory'> The group link is added by another group member, your registration will not be processed."
          -- bob <## ""
          -- bob <## "Please update the group profile yourself."
          -- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!")
          -- bob <## "welcome message changed to:"
          -- bob <## (welcomeWithLink <> " - welcome!")
          bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath."
          bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
          -- cath <## "bob updated group #privacy:"
          -- cath <## "welcome message changed to:"
          -- cath <## (welcomeWithLink <> " - welcome!")
          reapproveGroup 3 superUser bob
          groupFoundN 3 dan "privacy"

testDuplicateAskConfirmation :: HasCallStack => TestParams -> IO ()
testDuplicateAskConfirmation ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        submitGroup bob "privacy" "Privacy"
        _ <- groupAccepted bob "privacy"
        cath `connectVia` dsLink
        submitGroup cath "privacy" "Privacy"
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
        cath <## "To confirm the registration, please send:"
        cath <# "'SimpleX Directory'> /confirm 1:privacy"
        cath #> "@'SimpleX Directory' /confirm 1:privacy"
        welcomeWithLink <- groupAccepted cath "privacy"
        groupNotFound bob "privacy"
        completeRegistration superUser cath "privacy" "Privacy" welcomeWithLink 2
        groupFound bob "privacy"

testDuplicateProhibitRegistration :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitRegistration ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        cath `connectVia` dsLink
        groupFound cath "privacy"
        _ <- submitGroup cath "privacy" "Privacy"
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."

testDuplicateProhibitConfirmation :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitConfirmation ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        submitGroup bob "privacy" "Privacy"
        welcomeWithLink <- groupAccepted bob "privacy"
        cath `connectVia` dsLink
        submitGroup cath "privacy" "Privacy"
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
        cath <## "To confirm the registration, please send:"
        cath <# "'SimpleX Directory'> /confirm 1:privacy"
        groupNotFound cath "privacy"
        completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
        groupFound cath "privacy"
        cath #> "@'SimpleX Directory' /confirm 1:privacy"
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."

testDuplicateProhibitWhenUpdated :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitWhenUpdated ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        submitGroup bob "privacy" "Privacy"
        welcomeWithLink <- groupAccepted bob "privacy"
        cath `connectVia` dsLink
        submitGroup cath "privacy" "Privacy"
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
        cath <## "To confirm the registration, please send:"
        cath <# "'SimpleX Directory'> /confirm 1:privacy"
        cath #> "@'SimpleX Directory' /confirm 1:privacy"
        welcomeWithLink' <- groupAccepted cath "privacy"
        groupNotFound cath "privacy"
        completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
        groupFound cath "privacy"
        cath ##> ("/set welcome privacy " <> welcomeWithLink')
        cath <## "welcome message changed to:"
        cath <## welcomeWithLink'
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
        cath ##> "/gp privacy security Security"
        cath <## "changed to #security (Security)"
        cath <# "'SimpleX Directory'> Thank you! The group link for ID 2 (security) is added to the welcome message."
        cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
        notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2
        approveRegistration superUser cath "security" 2
        groupFound bob "security"
        groupFound cath "security"

testDuplicateProhibitApproval :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitApproval ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        submitGroup bob "privacy" "Privacy"
        welcomeWithLink <- groupAccepted bob "privacy"
        cath `connectVia` dsLink
        submitGroup cath "privacy" "Privacy"
        cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
        cath <## "To confirm the registration, please send:"
        cath <# "'SimpleX Directory'> /confirm 1:privacy"
        cath #> "@'SimpleX Directory' /confirm 1:privacy"
        welcomeWithLink' <- groupAccepted cath "privacy"
        updateProfileWithLink cath "privacy" welcomeWithLink' 2
        notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2
        groupNotFound cath "privacy"
        completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
        groupFound cath "privacy"
        -- fails at approval, as already listed
        let approve = "/approve 2:privacy 1"
        superUser #> ("@'SimpleX Directory' " <> approve)
        superUser <# ("'SimpleX Directory'> > " <> approve)
        superUser <## "      The group ID 2 (privacy) is already listed in the directory."

testListUserGroups :: HasCallStack => TestParams -> IO ()
testListUserGroups ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        cath `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        connectUsers bob cath
        fullAddMember "privacy" "Privacy" bob cath GRMember
        joinGroup "privacy" cath bob
        cath <## "#privacy: member 'SimpleX Directory_1' is connected"
        cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
        cath <## "use @'SimpleX Directory' <message> to send messages"
        registerGroupId superUser bob "security" "Security" 2 2
        registerGroupId superUser cath "anonymity" "Anonymity" 3 1
        listUserGroup cath "anonymity" "Anonymity"
        -- with de-listed group
        groupFound cath "anonymity"
        cath ##> "/mr anonymity 'SimpleX Directory' member"
        cath <## "#anonymity: you changed the role of 'SimpleX Directory' to member"
        cath <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (anonymity) is changed to member."
        cath <## ""
        cath <## "The group is no longer listed in the directory."
        superUser <# "'SimpleX Directory'> The group ID 3 (anonymity) is de-listed (SimpleX Directory role is changed to member)."
        groupNotFound cath "anonymity"
        listGroups superUser bob cath

testCapthaScreening :: HasCallStack => TestParams -> IO ()
testCapthaScreening ps =
  withDirectoryService ps $ \superUser dsLink ->
    withNewTestChat ps "bob" bobProfile $ \bob ->
      withNewTestChat ps "cath" cathProfile $ \cath -> do
        bob `connectVia` dsLink
        registerGroup superUser bob "privacy" "Privacy"
        -- check default role
        bob #> "@'SimpleX Directory' /role 1"
        bob <# "'SimpleX Directory'> > /role 1"
        bob <## "      The initial member role for the group privacy is set to member"
        bob <## "Send /'role 1 observer' to change it."
        bob <## ""
        note <- getTermLine bob
        let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
        -- enable captcha
        bob #> "@'SimpleX Directory' /filter 1 captcha"
        bob <# "'SimpleX Directory'> > /filter 1 captcha"
        bob <## "      Spam filter settings for group privacy set to:"
        bob <## "- reject long/inappropriate names: disabled"
        bob <## "- pass captcha to join: enabled"
        bob <## ""
        bob <## "/'filter 1 name' - enable name filter"
        bob <## "/'filter 1 name captcha' - enable both"
        bob <## "/'filter 1 off' - disable filter"
        -- connect with captcha screen
        _ <- join cath groupLink
        cath ##> "/_send #1(_support) text 123" -- sending incorrect captcha
        cath <# "#privacy (support) 123"
        cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123"
        cath <## "      Incorrect text, please try again."
        captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
        sendCaptcha cath captcha
        cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
        cath <## "#privacy: member bob (Bob) is connected"
        bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
        bob <## "#privacy: new member cath is connected"
        cath #> "#privacy hello"
        bob <# "#privacy cath> hello"
        cath ##> "/l privacy"
        cath <## "#privacy: you left the group"
        cath <## "use /d #privacy to delete the group"
        bob <## "#privacy: cath left the group"
        cath ##> "/d #privacy"
        cath <## "#privacy: you deleted the group"
        -- change default role to observer
        bob #> "@'SimpleX Directory' /role 1 observer"
        bob <# "'SimpleX Directory'> > /role 1 observer"
        bob <## "      The initial member role for the group privacy is set to observer"
        bob <## ""
        bob <##. "Please note: it applies only to members joining via this link: https://"
        -- connect with captcha screen again, as observer
        captcha' <- join cath groupLink
        sendCaptcha cath captcha'
        -- message from cath that left
        pastMember <- dropStrPrefix "#privacy: 'SimpleX Directory' forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath
        cath <# ("#privacy " <> pastMember <> "> hello [>>]")
        cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
        cath <## "#privacy: member bob (Bob) is connected"
        bob <## "#privacy: 'SimpleX Directory' added cath_1 (Catherine) to the group (connecting...)"
        bob <## "#privacy: new member cath_1 is connected"
        cath ##> "#privacy hello"
        cath <## "#privacy: you don't have permission to send messages"
        (bob </)
        cath ##> "/ms privacy"
        cath <## "cath (Catherine): observer, you, connected"
        cath <## "'SimpleX Directory': admin, host, connected"
        cath <## "bob (Bob): owner, connected"
        cath <## (pastMember <> ": author, status unknown")
  where
    join cath groupLink = do
      cath ##> ("/c " <> groupLink)
      cath <## "connection request sent!"
      cath <## "#privacy: joining the group..."
      cath <## "#privacy: you joined the group, pending approval"
      cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service."
      cath <## ""
      cath <## "Send captcha text to join the group privacy."
      dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
    sendCaptcha cath captcha = do
      cath ##> ("/_send #1(_support) text " <> captcha)
      cath <# ("#privacy (support) " <> captcha)
      cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
      cath <## "      Correct, you joined the group privacy"
      cath <## "#privacy: you joined the group"

testRestoreDirectory :: HasCallStack => TestParams -> IO ()
testRestoreDirectory ps = do
  testListUserGroups ps
  restoreDirectoryService ps 3 3 $ \superUser _dsLink ->
    withTestChat ps "bob" $ \bob ->
      withTestChat ps "cath" $ \cath -> do
        bob <## "2 contacts connected (use /cs for the list)"
        bob
          <### [ "#privacy: connected to server(s)",
                 "#security: connected to server(s)"
               ]
        cath <## "2 contacts connected (use /cs for the list)"
        cath
          <### [ "#privacy: connected to server(s)",
                 "#anonymity: connected to server(s)"
               ]
        listGroups superUser bob cath
        groupFoundN 3 bob "privacy"
        groupFound bob "security"
        groupFoundN 3 cath "privacy"
        cath #> "@'SimpleX Directory' security"
        groupFoundN' 2 cath "security"

testCaptcha :: HasCallStack => TestParams -> IO ()
testCaptcha _ps = do
  let captcha = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrty"
  matchCaptchaStr captcha captcha `shouldBe` True
  matchCaptchaStr captcha "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" `shouldBe` True
  matchCaptchaStr "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" captcha `shouldBe` True
  matchCaptchaStr "OOIICPSUVWXZ" "OOIICPSUVWXZ" `shouldBe` True
  matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxz" `shouldBe` True
  matchCaptchaStr "0o1lcpsuvwxz" "OOIICPSUVWXZ" `shouldBe` True
  matchCaptchaStr "OOIICPSUVWXZ" "" `shouldBe` False
  matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwx" `shouldBe` False
  matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxzz" `shouldBe` False

listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO ()
listGroups superUser bob cath = do
  sendListCommand bob 2
  groupListing bob 1 "privacy" "Privacy" 3 "active"
  groupListing bob 2 "security" "Security" 2 "active"
  sendListCommand cath 1
  groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed"
  -- superuser lists all groups
  bob #> "@'SimpleX Directory' /last"
  bob <# "'SimpleX Directory'> > /last"
  bob <## "      You are not allowed to use this command"
  superUser #> "@'SimpleX Directory' /last"
  superUser <# "'SimpleX Directory'> > /last"
  superUser <## "      3 registered group(s)"
  memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active"
  memberGroupListing superUser bob 2 "security" "Security" 2 "active"
  memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed"
  -- showing last 1 group
  superUser #> "@'SimpleX Directory' /last 1"
  superUser <# "'SimpleX Directory'> > /last 1"
  superUser <## "      3 registered group(s), showing the last 1"
  memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed"

listUserGroup :: HasCallStack => TestCC -> String -> String -> IO ()
listUserGroup u n fn = do
  sendListCommand u 1
  groupListing u 1 n fn 2 "active"

sendListCommand :: HasCallStack => TestCC -> Int -> IO ()
sendListCommand u count = do
  u #> "@'SimpleX Directory' /list"
  u <# "'SimpleX Directory'> > /list"
  u <## ("      " <> show count <> " registered group(s)")

groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO ()
groupListing u = groupListing_ u Nothing

memberGroupListing :: HasCallStack => TestCC -> TestCC -> Int -> String -> String -> Int -> String -> IO ()
memberGroupListing su owner = groupListing_ su (Just owner)

groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO ()
groupListing_ su owner_ gId n fn count status = do
  su <# ("'SimpleX Directory'> " <> show gId <> ". " <> n <> " (" <> fn <> ")")
  su <## "Welcome message:"
  su <##. ("Link to join the group " <> n <> ": ")
  forM_ owner_ $ \owner -> do
    ownerName <- userName owner
    su <## ("Owner: " <> ownerName)
  su <## (show count <> " members")
  su <## ("Status: " <> status)
  su <## ("/'role " <> show gId <> "', /'filter " <> show gId <> "'")

reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO ()
reapproveGroup count superUser bob = do
  superUser <# "'SimpleX Directory'> bob submitted the group ID 1:"
  superUser <##. "privacy ("
  superUser <## "Welcome message:"
  superUser <##. "Link to join the group privacy: "
  superUser <## (show count <> " members")
  superUser <## ""
  superUser <## "To approve send:"
  superUser <# "'SimpleX Directory'> /approve 1:privacy 1"
  superUser #> "@'SimpleX Directory' /approve 1:privacy 1"
  superUser <# "'SimpleX Directory'> > /approve 1:privacy 1"
  superUser <## "      Group approved!"
  bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!"
  bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
  bob <## ""
  bob <## "Supported commands:"
  bob <## "/'filter 1' - to configure anti-spam filter."
  bob <## "/'role 1' - to set default member role."
  bob <## "/'link 1' - to view/upgrade group link."

addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO ()
addCathAsOwner bob cath = do
  connectUsers bob cath
  fullAddMember "privacy" "Privacy" bob cath GROwner
  joinGroup "privacy" cath bob
  cath <## "#privacy: member 'SimpleX Directory' is connected"

withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryService ps = withDirectoryServiceCfg ps testCfg

withDirectoryServiceCfg :: HasCallStack => TestParams -> ChatConfig -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False

withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup test = do
  dsLink <-
    withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds ->
      withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do
        connectUsers ds superUser
        when createOwnersGroup $ do
          superUser ##> "/g owners"
          superUser <## "group #owners is created"
          superUser <## "to add members use /a owners <name> or /create link #owners"
          superUser ##> "/a owners 'SimpleX Directory' admin"
          superUser <## "invitation to join the group #owners sent to 'SimpleX Directory'"
          ds <## "#owners: alice invites you to join the group as admin"
          ds <## "use /j owners to accept"
          ds ##> "/j owners"
          ds <## "#owners: you joined the group"
          superUser <## "#owners: 'SimpleX Directory' joined the group"
        ds ##> "/ad"
        getContactLink ds True
  withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test

restoreDirectoryService :: HasCallStack => TestParams -> Int -> Int -> (TestCC -> String -> IO ()) -> IO ()
restoreDirectoryService ps ctCount grCount test = do
  dsLink <-
    withTestChat ps serviceDbPrefix $ \ds -> do
      ds <## (show ctCount <> " contacts connected (use /cs for the list)")
      ds <## "Your address is active! To show: /sa"
      ds <## (show grCount <> " group links active")
      forM_ [1 .. grCount] $ \_ -> ds <##. "#"
      ds ##> "/sa"
      dsLink <- getContactLink ds False
      ds <## "auto_accept on"
      pure dsLink
  withDirectory ps testCfg dsLink test

withDirectory :: HasCallStack => TestParams -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO ()
withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False

withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test = do
  let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] $ if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing
  runDirectory cfg opts $
    withTestChatCfg ps cfg "super_user" $ \superUser -> do
      superUser <## "1 contacts connected (use /cs for the list)"
      when createOwnersGroup $
        superUser <## "#owners: connected to server(s)"
      test superUser dsLink

runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
  st <- restoreDirectoryStore directoryLog
  t <- forkIO $ bot st
  threadDelay 500000
  action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
  where
    bot st = do
      env <- newServiceState opts
      let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}}
      simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env

registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
registerGroup su u n fn = registerGroupId su u n fn 1 1

registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO ()
registerGroupId su u n fn gId ugId = do
  submitGroup u n fn
  welcomeWithLink <- groupAccepted u n
  completeRegistrationId su u n fn welcomeWithLink gId ugId

submitGroup :: TestCC -> String -> String -> IO ()
submitGroup u n fn = do
  u ##> ("/g " <> viewName n <> if null fn then "" else " " <> fn)
  u <## ("group #" <> viewName n <> (if null fn then "" else " (" <> fn <> ")") <> " is created")
  u <## ("to add members use /a " <> viewName n <> " <name> or /create link #" <> viewName n)
  u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin")
  u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'")

groupAccepted :: TestCC -> String -> IO String
groupAccepted u n = do
  u <# ("'SimpleX Directory'> Joining the group " <> n <> "…")
  u <## ("#" <> viewName n <> ": 'SimpleX Directory' joined the group")
  u <# ("'SimpleX Directory'> Joined the group " <> n <> ", creating the link…")
  u <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
  u <## ""
  u <## "Please add it to the group welcome message."
  u <## "For example, add:"
  dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u -- welcome message with link

completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
completeRegistration su u n fn welcomeWithLink gId =
  completeRegistrationId su u n fn welcomeWithLink gId gId

completeRegistrationId :: TestCC -> TestCC -> String -> String -> String -> Int -> Int -> IO ()
completeRegistrationId su u n fn welcomeWithLink gId ugId = do
  updateProfileWithLink u n welcomeWithLink ugId
  notifySuperUser su u n fn welcomeWithLink gId
  approveRegistrationId su u n gId ugId

updateProfileWithLink :: TestCC -> String -> String -> Int -> IO ()
updateProfileWithLink u n welcomeWithLink ugId = do
  u ##> ("/set welcome " <> viewName n <> " " <> welcomeWithLink)
  u <## "welcome message changed to:"
  u <## welcomeWithLink
  u <# ("'SimpleX Directory'> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.")
  u <## "You will be notified once the group is added to the directory - it may take up to 48 hours."

notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
notifySuperUser su u n fn welcomeWithLink gId = do
  uName <- userName u
  su <# ("'SimpleX Directory'> " <> uName <> " submitted the group ID " <> show gId <> ":")
  su <## (n <> if null fn then "" else " (" <> fn <> ")")
  su <## "Welcome message:"
  su <## welcomeWithLink
  su .<## "members"
  su <## ""
  su <## "To approve send:"
  let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1"
  su <# ("'SimpleX Directory'> " <> approve)

approveRegistration :: TestCC -> TestCC -> String -> Int -> IO ()
approveRegistration su u n gId =
  approveRegistrationId su u n gId gId

approveRegistrationId :: TestCC -> TestCC -> String -> Int -> Int -> IO ()
approveRegistrationId su u n gId ugId = do
  let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1"
  su #> ("@'SimpleX Directory' " <> approve)
  su <# ("'SimpleX Directory'> > " <> approve)
  su <## "      Group approved!"
  u <# ("'SimpleX Directory'> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!")
  u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
  u <## ""
  u <## "Supported commands:"
  u <## ("/'filter " <> show ugId <> "' - to configure anti-spam filter.")
  u <## ("/'role " <> show ugId <> "' - to set default member role.")
  u <## ("/'link " <> show ugId <> "' - to view/upgrade group link.")

connectVia :: TestCC -> String -> IO ()
u `connectVia` dsLink = do
  u ##> ("/c " <> dsLink)
  u <## "connection request sent!"
  u .<## ": contact is connected"
  u .<# "> Welcome to SimpleX Directory!"
  u <## ""
  u <## "🔍 Send search string to find groups - try security."
  u <## "/help - how to submit your group."
  u <## "/new - recent groups."
  u <## ""
  u <## "[Directory rules](https://simplex.chat/docs/directory.html)."

joinGroup :: String -> TestCC -> TestCC -> IO ()
joinGroup gName member host = do
  let gn = "#" <> gName
  memberName <- userName member
  hostName <- userName host
  member ##> ("/j " <> gName)
  member <## (gn <> ": you joined the group")
  member <#. (gn <> " " <> hostName <> "> Link to join the group " <> gName <> ": ")
  host <## (gn <> ": " <> memberName <> " joined the group")

leaveGroup :: String -> TestCC -> IO ()
leaveGroup gName member = do
  let gn = "#" <> gName
  member ##> ("/l " <> gName)
  member <## (gn <> ": you left the group")
  member <## ("use /d " <> gn <> " to delete the group")

removeMember :: String -> TestCC -> TestCC -> IO ()
removeMember gName admin removed = do
  let gn = "#" <> gName
  adminName <- userName admin
  removedName <- userName removed
  admin ##> ("/rm " <> gName <> " " <> removedName)
  admin <## (gn <> ": you removed " <> removedName <> " from the group")
  removed <## (gn <> ": " <> adminName <> " removed you from the group")
  removed <## ("use /d " <> gn <> " to delete the group")

groupFound :: TestCC -> String -> IO ()
groupFound = groupFoundN 2

groupFoundN :: Int -> TestCC -> String -> IO ()
groupFoundN count u name = do
  u #> ("@'SimpleX Directory' " <> name)
  groupFoundN' count u name

groupFoundN' :: Int -> TestCC -> String -> IO ()
groupFoundN' = groupFoundN_ "" Nothing

groupFoundN_ :: String -> Maybe Int -> Int -> TestCC -> String -> IO ()
groupFoundN_ suffix shownId_ count u name = do
  u <# ("'SimpleX Directory" <> suffix <> "'> > " <> name)
  u <## "      Found 1 group(s)."
  u <#. ("'SimpleX Directory" <> suffix <> "'> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name)
  u <## "Welcome message:"
  u <##. "Link to join the group "
  u <## (show count <> " members")

groupNotFound :: TestCC -> String -> IO ()
groupNotFound = groupNotFound_ ""

groupNotFound_ :: String -> TestCC -> String -> IO ()
groupNotFound_ suffix u s = do
  u #> ("@'SimpleX Directory" <> suffix <> "' " <> s)
  u <# ("'SimpleX Directory" <> suffix <> "'> > " <> s)
  u <## "      No groups found"
